<?php

/**
 * 分布式Kv表
 * W:吴振球
 *
 * 1.目标：通过读取配置文件，实现分库分表规则
 *
 * 2.实例化：操作与Table.php一样，如：
 *     SupportTable::instance->get(...);
 *
 * 3.配置文件:
 *      需要分库分表时，需设置好配置文件如：SupportTable的配置文件如下（conf/app_share/develop.db.php）
 * $post_sharding = array (
'db'              => 1,  //库数量
'table'           => 4,  //表数量
);
 * 4.配置名：
 *      配置名可在Dao层修改，对应配置文件返回的名字
 *   private $DDB_conf_name = 'post_sharding';
 *
 * 5.Dao层对库名和表名的要求:
 *   Dao层库名：dev_csp，+配置文件（4库）后最终库名：dev_csp_0,dev_csp_1,dev_csp_2,dev_csp_3,
 *   Dao层表名：post_suppert，+配置文件（4表）后最终表名：post_suppert_0,post_suppert_1,post_suppert_2,post_suppert_3,
 *
 * 6.总结：
 *      配置文件+Dao层配置名=DDBTable
 *
 */
namespace Lavender\Dao;

use Lavender\Core;
use Lavender\Errno;

/**
 * key value table access class
 */
abstract class DDBKvTable
{
    const FLAG_SERIALIZE = 0x1;
    /**
     * database system
     *
     * @var string
     */
    protected $driver = 'mysql';

    /**
     * database config name
     *
     * @var string
     */
    protected $database = 'demo_database';

    /**
     * first key field name
     *
     * @var string
     */
    protected $first_key = 'id';

    /**
     * table
     *
     * @var string
     */
    protected $table = 'kv_demo_table';

    //
    protected $db_handle;

    // set config name
    protected $DDB_conf_name = 'post_sharding';
    /**
     * all instances cache
     *
     * @var array
     */
    private static $instances = array();

    /**
     * Sets of table=>id
     * array(
     * table1=>array(id1,id2,id3),
     * table2=>array(......),
     * );
     */
    private $tables = array();

    /**
     * Sets of database=>table
     * array(
     * db1=>array(tb1,tb2,tb3),
     * db2=>array(......),
     * );
     */
    private $dbs = array();

    /***
     * @var bool 同步开关
     */
    public $is_sync=true;

    //
    public function __construct()
    {
        //
    }

    /**
     * sharding rule
     *
     * @param string|int $sharding_id
     */
    public function sharding_rule($sharding_id)
    {
        if (is_null($sharding_id)) {
            throw new Exception ('param missed', Errno::PARAM_MISSED);
        }
        // if not array ,array $sharding_id
        if (!is_array($sharding_id)) {
            $sharding_id = array(
                $sharding_id
            );
        }
        $sharding_id = array_unique($sharding_id);
        $conf = Core::get_config('db', $this->DDB_conf_name);
        foreach ($sharding_id as $vs) {
            // set database name
            $index = $vs % $conf ['db'];
            $database_suffix = '_' . $index;

            // set table name
            $n = ($vs / $conf['db']) % $conf ['table'];
            $table = $this->table . '_' . $n;

            // Sets of database and table
            $dbs [] = array(
                $database_suffix => $table
            );
            // Sets of table and id
            $tables [] = array(
                $table => $vs
            );
        }
        $this->dbs = self::array_two($dbs);
        $this->tables = self::array_two($tables);
    }

    /**
     * make rule for an array
     *
     * @param array $two
     * @return multitype:
     */
    public function array_two(array $two)
    {
        foreach ($two as $k => $v) {
            foreach ($v as $k1 => $v1) {
                $ks [] = $k1;
            }
        }
        // unique array key
        $ks = array_unique($ks);
        foreach ($two as $k => $v) {
            foreach ($ks as $v3) {
                if ($two [$k] [$v3] != null) {
                    $res [$v3] [] = $two [$k] [$v3];
                }
            }
        }
        foreach ($res as $k4 => $v4) {
            // unique array value
            $result [$k4] = array_unique($res [$k4]);
        }
        return $result;
    }

    /**
     * get instance singletion
     *
     * @return Table
     */
    public static function instance()
    {
        $class = get_called_class();
        if (empty (self::$instances [$class])) {
            self::$instances [$class] = new $class ();
        }
        return self::$instances [$class];
    }

    /**
     *
     * @param unknown $id
     * @throws Exception
     * @return NULL unknown
     */
    public function get($id)
    {
        if (is_null($id)) {
            throw new Exception ('param error', Errno::PARAM_INVALID);
        }
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                // 得到当前$table下的ids
                foreach ($this->tables[$table] as $v_id) {
                    $condition = is_numeric($v_id) ? "`{$this->first_key}`='{$v_id}'" : "`{$this->first_key}`='" . $this->db_handle->escape($v_id) . "'";
                    return $this->unpack($this->db_handle->get($table, $condition)[0]['data']);
                }
            }
        }
    }

    /**
     * get the record's raw data
     *
     * @param int $id
     * @return array
     */
    public function get_raw_record($id)
    {
        if (is_null($id)) {
            throw new Exception ('param error', Errno::PARAM_INVALID);
        }
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                // 得到当前$table下的的id
                $condition = is_numeric($this->tables [$table]) ? "`{$this->first_key}`={$this->tables [$table]}" : "`{$this->first_key}`='" . $this->db_handle->escape($this->tables [$table]) . "'";
                $items = $this->db_handle->get($table, $condition);
            }
        }
        if (empty ($items)) {
            return null;
        }
        $item = $items [0];
        if ($item ['flags'] & self::FLAG_SERIALIZE) {
            $data = $this->unpack($item ['data']);
            if ($data === false) {
                throw new Exception ('unserialize failed,data:' . $item ['data'], Errno::UNPACK_FAILED);
            }

            $item ['data'] = $data;
        }
        return $item;
    }

    /**
     * insert an record
     *
     * @param array $record
     * @return int
     */
    public function add($id, $data, $time = 0)
    {
        if (is_null($id)) {
            throw new Exception ('param error', Errno::PARAM_INVALID);
        }
        $flags = 0;
        if (!is_string($data) && !is_int($data) && !is_float($data)) {
            $flags |= self::FLAG_SERIALIZE;
            $data = $this->pack($data);
        }
        $record = array(
            $this->first_key => $id,
            'flags' => $flags,
            'data' => $data,
            'updated' => $time ? $time : time()
        );
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                $this->db_handle->insert($table, $record);
            }
        }

        //数据同步
        self::set_sync_data(get_called_class(),__FUNCTION__,func_get_args());
    }

    public function set($id, $data, $time = 0)
    {
        if (is_null($id)) {
            throw new Exception ('param error', Errno::PARAM_INVALID);
        }

        $flags = 0;
        if (!is_string($data) && !is_int($data) && !is_float($data)) {
            $flags |= self::FLAG_SERIALIZE;
            $data = $this->pack($data);
        }
        $update_record = array(
            'flags' => $flags,
            'data' => $data,
            'updated' => $time ? $time : time()
        );

        $insert_record = array(
            $this->first_key => $id,
            'flags' => $flags,
            'data' => $data,
            'updated' => $time ? $time : time()
        );
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                $this->db_handle->insert_or_update($table, $insert_record, $update_record);
            }
        }

        //数据同步
        self::set_sync_data(get_called_class(),__FUNCTION__,func_get_args());
    }

    /**
     * update a record
     *
     * @param string $id
     * @param array $record
     * @param int $time
     * @return mixed num on success,false on failed
     */
    public function update($id, $data, $time = 0)
    {
        if (is_null($id)) {
            throw new Exception ('param error', Errno::PARAM_INVALID);
        }
        $flags = 0;
        if (!is_string($data) && !is_int($data) && !is_float($data)) {
            $flags |= self::FLAG_SERIALIZE;
            $data = $this->pack($data);
        }
        $update_record = array(
            'flags' => $flags,
            'data' => $data,
            'updated' => $time ? $time : time()
        );
        $id = $this->db_handle->escape($id);
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                $this->db_handle->update($table, $update_record, "{$this->first_key}='{$this->tables[$table][0]}'");
            }
        }

        //数据同步
        self::set_sync_data(get_called_class(),__FUNCTION__,func_get_args());

        return $this->db_handle->get_affected_rows();
    }

    /**
     * update the record modify time
     *
     * @param string $id
     * @param int $time
     * @return mixed updated num on success,false on failed
     */
    public function update_time($id, $time = 0)
    {
        if (is_null($id)) {
            throw new Exception ('param error', Errno::PARAM_INVALID);
        }
        $update_record = array(
            'updated' => $time ? $time : time()
        );
        $id = $this->db_handle->escape($id);
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                $this->db_handle->update($table, $update_record, "{$this->first_key}='{$this->tables[$table][0]}'");
            }
        }
        return $this->db_handle->get_affected_rows();
    }

    /**
     * delete record
     *
     * @param string $id
     * @return mixed deleted num on success,false on failed
     */
    public function delete($id)
    {
        if (is_null($id)) {
            throw new Exception ('param error', Errno::PARAM_INVALID);
        }

        $id = $this->db_handle->escape($id);
        // 得到分库分表规则
        self::sharding_rule($id);
        // 循环实例化数据库
        foreach ($this->dbs as $database_suffix => $tables) {
            $this->db_handle = Core::get_database($this->driver, $this->database, $database_suffix);
            foreach ($tables as $table) {
                $this->db_handle->delete($table, "{$this->first_key}='{$this->tables[$table][0]}'");
            }
        }

        //数据同步
        self::set_sync_data(get_called_class(),__FUNCTION__,func_get_args());

        return $this->db_handle->get_affected_rows();
    }

    protected function pack($data)
    {
        // return serialize($data);
        return json_encode($data, JSON_UNESCAPED_UNICODE);
    }

    protected function unpack($data)
    {
        // return unserialize($data);
        return json_decode($data, true);
    }


    protected   function  set_sync_data($called_class,$function,array $params){
        //数据同步
//        if ($this->db_handle->get_affected_rows()){
//            if ($this->is_sync) {
//                $sync_table=Core::get_config('const','sync_table');
//                if ($sync_table && in_array($called_class,$sync_table)){
//                    \Golo\Api\DataSync::send(L_APP_NAME,$called_class,$function, $params);
//                }
//            }
//        }
    }
}
